import torch
from torchvision import transforms, datasets
from torch.utils.data import DataLoader
import torch.nn.functional as F

BATCH_SIZE = 64
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.1307, ), (0.3081, ))
])

train_dataset = datasets.MNIST(
    root = "../dataset/mnist",
    train=True,
    download=True,
    transform=transform
)
train_loader = DataLoader(
    dataset=train_dataset,
    batch_size=BATCH_SIZE,
    shuffle=True,
    num_workers=2
)

test_dataset = datasets.MNIST(
    root="../data/mnist",
    train=False,
    download=True,
    transform=transform
)
test_loader = DataLoader(
    dataset=test_dataset,
    batch_size=BATCH_SIZE,
    shuffle=False,
    num_workers=2
)

# 定义残差网络 Res-Net
class ResNet(torch.nn.Module):
    def __init__(self, channels):
        super(ResNet, self).__init__()
        self.conv1 = torch.nn.Conv2d(in_channels=channels, out_channels=channels, kernel_size=3, padding=1)
        self.conv2 = torch.nn.Conv2d(in_channels=channels, out_channels=channels, kernel_size=3, padding=1)


    def forward(self, x):
        y = F.relu(self.conv1(x))
        y = self.conv2(y)
        y = F.relu(y + x)
        return y

class Net(torch.nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = torch.nn.Conv2d(in_channels=1, out_channels=16, kernel_size=5)
        self.conv2 = torch.nn.Conv2d(in_channels=16, out_channels=32, kernel_size=5)

        self.maxpool = torch.nn.MaxPool2d(2)
        self.resnet1 = ResNet(16)
        self.resnet2 = ResNet(32)

        self.linear = torch.nn.Linear(512, 10)

    def forward(self, x):
        batch_size = x.size(0)
        x = self.resnet1(self.maxpool(F.relu(self.conv1(x))))  # 卷积-激活-池化-残差
        x = self.resnet2(self.maxpool(F.relu(self.conv2(x))))
        x = x.view(batch_size, -1) # 展平
        x = self.linear(x)
        return x

model = Net()
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model = model.to((device))

criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.5)

def train(epoch):
    running_loss = 0.0
    for batch_idx, data in enumerate(train_loader, 0):
        optimizer.zero_grad()
        inputs, targets = data
        # 将输入与对应标签放到 GPU 上
        inputs, targets = inputs.to(device), targets.to(device)

        y_pred = model(inputs)
        loss = criterion(y_pred, targets)
        loss.backward()
        optimizer.step()

        running_loss += loss.item()
        if batch_idx % 300 == 299:
            print('[%d, %5d] loss: %.3f' % (epoch + 1, batch_idx + 1, running_loss / 300))


def test():
    correct = 0.0
    total = 0
    with torch.no_grad():
        for batch_idx, data in enumerate(train_loader):
            inputs, labels = data
            inputs, labels = inputs.to(device), labels.to(device)
            outputs = model(inputs)
            _, predicted = torch.max(outputs.data, dim=1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()
    print('Accuracy on test set: %d %%' % (100 * correct / total))


if __name__ == "__main__":
    for epoch in range(10):
        train(epoch)
        test()